﻿namespace MicroFrameworkDriverLibrary.Devices
{
    using System;
    using Microsoft.SPOT;
    using Microsoft.SPOT.Hardware;
    using MicroFrameworkDriverLibrary.Interfaces;

    /// <summary>
    /// MCP23008 - i2c 8 input/output port expander
    /// 
    /// Use this chip from 2.7-5.5V (good for any 3.3V or 5V setup), and you can sink/source up to 20mA from any of the I/O pins.
    /// </summary>
    /// <remarks><![CDATA[
    /// MCP23008 (N) DIP18 pin layout:
    /// 
    ///   18 17 16 15 14 13 12 11 10 
    ///   │  │  │  │  │  │  │  │  │  
    /// █████████████████████████████
    /// ▀████████████████████████████
    ///   ███████████████████████████
    /// ▄████████████████████████████
    /// █████████████████████████████
    ///   │  │  │  │  │  │  │  │  │  
    ///   1  2  3  4  5  6  7  8  9  
    /// 
    /// 1   SCL  [See Note]  (Pullup Required)  18 VDD   (3.3v)
    /// 2   SDA  [See Note]  (Pullup Required)  17 GP7
    /// 3   A2                                  16 GP6
    /// 4   A2                                  15 GP5
    /// 5   A0                                  14 GP4 
    /// 6   RESET (+Pullup)                     13 GP3 
    /// 7   NC                                  12 GP2 
    /// 8   INT                                 11 GP1
    /// 9   VSS  (GND)                          10 GP0
    ///
    /// NOTE:
    ///   SCL: Netdiono Pin 4, FEZ Cobra Pin IO12
    ///   SDA: Netduino Pin 5, FEZ Cobra Pin IO11
    /// 
    ///   Hardware Address: [0 = GND, 1 = +Pullup)
    ///     A0/A1/A2
    ///     --------
    ///     0  0  0   = 0x20
    ///     1  0  0   = 0x21
    ///     0  1  0   = 0x22
    ///     1  1  0   = 0x23
    ///     0  0  1   = 0x24
    ///     1  0  1   = 0x25
    ///     0  1  1   = 0x26
    ///     1  1  1   = 0x27
    /// 
    /// ]]></remarks>
    public sealed class MCP23008 : I2CDeviceHelper
    {
        private const ushort HARDWARE_BASE_ADDRESS = 0x20;

        private const byte MCP23008_IODIR = 0x00;
        private const byte MCP23008_IPOL = 0x01;
        private const byte MCP23008_GPINTEN = 0x02;
        private const byte MCP23008_DEFVAL = 0x03;
        private const byte MCP23008_INTCON = 0x04;
        private const byte MCP23008_IOCON = 0x05;
        private const byte MCP23008_GPPU = 0x06;
        private const byte MCP23008_INTF = 0x07;
        private const byte MCP23008_INTCAP = 0x08;
        private const byte MCP23008_GPIO = 0x09;
        private const byte MCP23008_OLAT = 0x0A;

        private enum IODirection
        {
            Output = 0,
            Input = 1
        }
        private enum Polarity
        {
            Normal = 0,
            Invert = 1,
        }
        private enum ConfigurationOption
        {
            /// <summary>
            /// Sequential Operation
            /// 0 = enabled(default)
            /// 1 = disabled
            /// </summary>
            SEQOP = 0x20,

            /// <summary>
            /// SDA Slew Rate
            /// 0 = enabled(default)
            /// 1 = disabled
            /// </summary>
            DISSLW = 0x10,

            /// <summary>
            /// Hardware Address Enable for MCP23S08 SPI version only
            /// 0 = disabled(default)
            /// 1 = enabled
            /// </summary>
            HAEN = 0x08,

            /// <summary>
            /// INT pin as open-drain
            /// 0 = Active driver output(default)
            /// 1 = Open-drain output
            /// </summary>
            ODR = 0x04,

            /// <summary>
            /// INT polarity
            /// 0 = Active-low(default)
            /// 1 = Active-high
            /// </summary>
            INTPOL = 0x02,
        }

        private IMCP23008DevicePort[] _devicePorts = new IMCP23008DevicePort[8];  // There are a total of 8 i/o ports that can be created for this driver.

        public MCP23008(I2CDevice sharedI2C, int clockRateKHz = 400, int transacitonTimeoutMs = 50, ushort hardwareAddress = HARDWARE_BASE_ADDRESS)
            : base(sharedI2C, hardwareAddress, clockRateKHz, transacitonTimeoutMs)
        {
        }

        public void InitializeDevice()
        {
            this.SwapConfiguration(() =>
            {
                this.WriteRegister(MCP23008_IODIR, 0xFF);
                this.WriteRegister(MCP23008_IPOL, 0x00);
                this.WriteRegister(MCP23008_GPINTEN, 0x00);
                this.WriteRegister(MCP23008_DEFVAL, 0x00);
                this.WriteRegister(MCP23008_INTCON, 0x00);
                this.WriteRegister(MCP23008_IOCON, 0x00);
                this.WriteRegister(MCP23008_GPPU, 0x00);
                this.WriteRegister(MCP23008_INTF, 0x00);
                this.WriteRegister(MCP23008_INTCAP, 0x00);
                this.WriteRegister(MCP23008_GPIO, 0x00);
                this.WriteRegister(MCP23008_OLAT, 0x00);
            });
        }

        public IOutputPort CreateOutputPort(Cpu.Pin portId, bool initialState)
        {
            if (this._devicePorts[(int)portId] != null)
            {
                // This device port is already in use.
            }

            var port = new MCP23008OutputPort(this, portId, initialState);
            this._devicePorts[(int)portId] = port;
            return port;
        }
        public IInputPort CreateInputPort(Cpu.Pin portId, bool glitchFilter, Port.ResistorMode resistorMode)
        {
            if (this._devicePorts[(int)portId] != null)
            {
                // This device port is already in use.
            }

            var port = new MCP23008InputPort(this, portId, glitchFilter, resistorMode);
            this._devicePorts[(int)portId] = port;
            return port;
        }

        protected override void Dispose(bool disposing)
        {
            base._disposed = true;
        }

        private void Write(Cpu.Pin pin, bool value)
        {
            this.WriteRegister(MCP23008_GPIO, pin, value);
        }

        private bool Read(Cpu.Pin pin)
        {
            return this.ReadRegister(MCP23008_GPIO, pin);
        }

        private void SetPinDirection(Cpu.Pin pin, IODirection direction)
        {
            this.WriteRegister(MCP23008_IODIR, pin, direction == IODirection.Input);
        }

        private void SetPinPolarity(Cpu.Pin pin, Polarity polarity)
        {
            this.WriteRegister(MCP23008_GPINTEN, pin, polarity == Polarity.Invert);
        }

        private void WriteRegister(byte register, byte value)
        {
            // This method assumes the the configuration has already been setup. So don't do it again.
            var result = this._sharedI2C.Execute(new I2CDevice.I2CTransaction[] 
            { 
                I2CDevice.CreateWriteTransaction(new byte[] { register, value })
            }, this._transactionTimeoutMs);
        }

        private void WriteRegister(byte register, Cpu.Pin pin, bool enable)
        {
            this.SwapConfiguration(() =>
            {
                // First read the current value
                byte currentValue = this.ReadRegister(register);

                // Set the pins contents to the new value
                currentValue = SetBit(currentValue, (byte)pin, enable);

                // Write the new value
                this.WriteRegister(register, currentValue);
            });
        }

        private byte ReadRegister(byte register)
        {
            // This method assumes the the configuration has already been setup. So don't do it again.
            byte[] readBuffer = new byte[1];
            var result = this._sharedI2C.Execute(new I2CDevice.I2CTransaction[] 
            { 
                I2CDevice.CreateWriteTransaction(new byte[] { register }),
                I2CDevice.CreateReadTransaction(readBuffer)
            }, this._transactionTimeoutMs);

            return readBuffer[0];
        }

        private bool ReadRegister(byte register, Cpu.Pin pin)
        {
            bool result = false;
            this.SwapConfiguration(() =>
            {
                var currentValue = this.ReadRegister(register);
                result = GetBit(currentValue, (byte)pin);
            });
            return result;
        }

        private void SetPinPullup(Cpu.Pin pin, bool value)
        {
            WriteRegister(MCP23008_GPPU, pin, value);
        }

        private void SetConfigurationOption(ConfigurationOption value)
        {
            this.SwapConfiguration(() =>
            {
                this.WriteRegister(MCP23008_IOCON, (byte)value);
            });
        }

        private interface IMCP23008DevicePort { }

        private sealed class MCP23008OutputPort : IMCP23008DevicePort, IOutputPort, IDisposable
        {

            public MCP23008OutputPort(MCP23008 owner, Cpu.Pin portId, bool initialState)
            {
                this.Owner = owner;
                this.Id = portId;
                this.InitialState = initialState;

                this.Owner.Write(this.Id, initialState);
                this.Owner.SetPinDirection(this.Id, IODirection.Output);
            }

            private MCP23008 Owner { get; set; }

            public Cpu.Pin Id
            {
                get;
                private set;
            }

            public bool InitialState
            {
                get;
                private set;
            }

            public bool Read()
            {
                return this.Owner.Read(this.Id);
            }

            public void Write(bool value)
            {
                this.Owner.Write(this.Id, value);
            }

            void IDisposable.Dispose()
            {
            }
        }

        private sealed class MCP23008InputPort : IMCP23008DevicePort, IInputPort, IDisposable
        {
            public MCP23008InputPort(MCP23008 owner, Cpu.Pin portId, bool glitchFilter, Port.ResistorMode resistorMode)
            {
                this.Owner = owner;
                this.Id = portId;
                this.GlitchFilter = glitchFilter;
                this.Resistor = resistorMode;

                //this.Owner.Write(this.Id, initialState);
                this.Owner.SetPinDirection(this.Id, IODirection.Input);
            }

            private MCP23008 Owner { get; set; }

            public bool GlitchFilter
            {
                get
                {
                    throw new NotImplementedException();
                }
                set
                {
                    throw new NotImplementedException();
                }
            }

            public Cpu.Pin Id
            {
                get;
                private set;
            }

            public bool Read()
            {
                throw new NotImplementedException();
            }

            public Port.ResistorMode Resistor
            {
                get;
                set;
            }

            void IDisposable.Dispose()
            {
            }


            public void DisableInterrupt()
            {
                throw new NotImplementedException();
            }

            public void EnableInterrupt()
            {
                throw new NotImplementedException();
            }
        }
    }
}
